package com.hspedu.hspspringmvc.context;

import com.hspedu.hspspringmvc.annotation.AutoWired;
import com.hspedu.hspspringmvc.annotation.Controller;
import com.hspedu.hspspringmvc.annotation.Service;
import com.hspedu.hspspringmvc.xml.XMLParser;

import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * HspWebApplicationContext 表示自己的spring容器
 */
public class HspWebApplicationContext {
    //定义属性classFullPathList, 保存扫描包/子包的类的全路径
    private List<String> classFullPathList =
            new ArrayList<>();
    //定义属性ioc, 存放反射生成的Bean对象 /Controller/Service
    public ConcurrentHashMap<String, Object> ioc =
            new ConcurrentHashMap<>();

    //无参构造器
    public HspWebApplicationContext() {
    }

    private String configLocation;//属性,表示spring容器配置文件

    public HspWebApplicationContext(String configLocation) {
        this.configLocation = configLocation;
    }

    //编写方法,完成spring容器的初始化
    public void init() {

        String basePackage =
                XMLParser.getBasePackage(configLocation.split(":")[1]);
        String[] basePackages = basePackage.split(",");
        if (basePackages.length > 0) {
            for (String pack : basePackages) {
                scanPackage(pack);//扫描
            }
        }
        System.out.println("扫描后的= classFullPathList=" + classFullPathList);
        //将扫描到的类, 反射到ico容器
        executeInstance();
        System.out.println("扫描后的 ioc容器= " + ioc);

        //完成注入的bean对象,的属性的装配
        executeAutoWired();
        System.out.println("装配后 ioc容器= " + ioc);
    }


    /**
     * 创建方法,完成对包的扫描->考察大家对io/容器/字符串处理
     * @param pack 表示要扫描的包
     */
    public void scanPackage(String pack) {

        //得到包所在的工作路径[绝对路径]
        //下面这句话的含义是 通过类的加载器，得到你指定的包对应的 工作路径[绝对路径]
        //比如 "com.hspedu.controller" => url 是 D:\hspedu_springmvc\hsp-springmvc\target\hsp-springmvc\WEB-INF\classes\com\hspedu\controller
        //老师细节说明： 1. 不要直接使用Junit测试, 否则 url null
        //             2. 启动tomcat来测试
        URL url =
                this.getClass().getClassLoader()
                        .getResource("/" + pack.replaceAll("\\.", "/"));

        //System.out.println("url=" + url);
        //根据得到的路径, 对其进行扫描，把类的全路径，保存到classFullPathList

        String path = url.getFile();
        System.out.println("path= " + path);
        //在io中,把目录，视为一个文件
        File dir = new File(path);
        //遍历dir[文件/子目录]
        for (File f : dir.listFiles()) {
            if (f.isDirectory()) {//如果是一个目录，需要递归扫描
                scanPackage(pack + "." + f.getName());
            } else {
                //老韩说明:这时，你扫描到的文件，可能是.class, 也可能是其它文件
                //就算是.class, 也存在是不是需要注入到容器
                //目前先把文件的全路径都保存到集合，后面在注入对象到容器时，再处理
                String classFullPath =
                        pack + "." + f.getName().replaceAll(".class", "");
                classFullPathList.add(classFullPath);
            }
        }

    }


    //编写方法,将扫描到的类, 在满足条件的情况下，反射到ioc容器
    public void executeInstance() {
        //判断是否扫描到类
        if (classFullPathList.size() == 0) {//说明没有扫描到类
            return;
        }
        try {
            //遍历classFullPathList,进行反射
            for (String classFullPath : classFullPathList) {
                Class<?> clazz = Class.forName(classFullPath);
                //说明当前这个类有@Controller
                if (clazz.isAnnotationPresent(Controller.class)) {
                    //得到类名首字母小写
                    String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase() +
                            clazz.getSimpleName().substring(1);
                    ioc.put(beanName, clazz.newInstance());
                } //如果有其它的注解，可以扩展 , 来处理@Service
                else if (clazz.isAnnotationPresent(Service.class)) {//如果类有@Serivce注解

                    //先获取到Service的value值=> 就是注入时的beanName
                    Service serviceAnnotation =
                            clazz.getAnnotation(Service.class);

                    String beanName = serviceAnnotation.value();
                    if ("".equals(beanName)) {//说明没有指定value, 我们就使用默认的机制注入Service
                        //可以通过接口名/类名[首字母小写]来注入ioc容器
                        //1.得到所有接口的名称=>反射
                        Class<?>[] interfaces = clazz.getInterfaces();

                        Object instance = clazz.newInstance();
                        //2. 遍历接口，然后通过多个接口名来注入
                        for (Class<?> anInterface : interfaces) {
                            //接口名->首字母小写
                            String beanName2 = anInterface.getSimpleName().substring(0, 1).toLowerCase() +
                                    anInterface.getSimpleName().substring(1);
                            ioc.put(beanName2, instance);
                        }
                        //3. 留一个作业,使用类名的首字母小写来注入bean
                        //   通过 clazz 来即可.

                    } else {//如果有指定名称,就使用该名称注入即可
                        ioc.put(beanName, clazz.newInstance());
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    //编写方法，完成属性的自动装配
    public void executeAutoWired() {
        //判断ioc有没有要装配的对象
        if (ioc.isEmpty()) {
            return; //你也可以抛出异常 throw new RuntimeException("ioc 容器没有bean对象")
        }
        //遍历ioc容器中的所有注入的bean对象, 然后获取到bean的所有字段/属性，判断是否需要
        //装配
        /**
         * entry => <String,Object > String 就是你注入对象时名称 Object就是bean对象
         */
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {

            //String key = entry.getKey();
            Object bean = entry.getValue();

            //得到bean的所有字段/属性
            Field[] declaredFields = bean.getClass().getDeclaredFields();
            for (Field declaredField : declaredFields) {
                //判断当前这个字段，是否有@AutoWired
                if (declaredField.isAnnotationPresent(AutoWired.class)) {//有@AutoWired

                    //的当前这个字段的@AutoWired
                    AutoWired autoWiredAnnotation = declaredField.getAnnotation(AutoWired.class);
                    String beanName = autoWiredAnnotation.value();
                    if ("".equals(beanName)) {//如果没有设置value,按照默认规则
                        //即得到字段类型的名称的首字母小写，作为名字来进行装配
                        Class<?> type = declaredField.getType();
                        beanName = type.getSimpleName().substring(0, 1).toLowerCase() +
                                type.getSimpleName().substring(1);
                    }
                    //如果设置value, 直接按照beanName来进行装配
                    //从ioc容器中获取到bean
                    if (null == ioc.get(beanName)) {//说明你指定的名字对应的bean不在ioc容器
                        throw new RuntimeException("ioc容器中, 不存在你要装配的bean");
                    }
                    //防止属性是private, 我们需要暴力破解
                    declaredField.setAccessible(true);
                    //可以装配属性
                    try {
                        declaredField.set(bean, ioc.get(beanName));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }
            }

        }

    }
}
